﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core
{
	/// <summary>Base class for the emitters of types.</summary>
	public abstract class TypeEmitterBase<T_OPTIONS> where T_OPTIONS : TypeEmitterOptions
	{
		#region Inner Types
		/// <summary>Represents a method able to emit a property (either a get or a set) body.</summary>
		/// <param name="ilGenerator"></param>
		/// <param name="method"></param>
		public delegate void EmitMethodHandler(ILGeneratorExtender ilGenerator, MethodInfo method);

		/// <summary>Represents a method able to emit a method body.</summary>
		/// <param name="ilGenerator"></param>
		/// <param name="method"></param>
		/// <param name="property"/>
		public delegate void EmitPropertyHandler(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property);

		/// <summary>Represents a method able to emit a method body.</summary>
		/// <param name="ilGenerator"></param>
		/// <param name="method"></param>
		/// <param name="eventInfo"/>
		public delegate void EmitEventHandler(ILGeneratorExtender ilGenerator, MethodInfo method, EventInfo eventInfo);
		#endregion

		private const BindingFlags BASE_FIND_BINDING_FLAGS =
			BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic;

		protected const string ADAPTEE_FIELD_NAME = "_adaptee";
		protected const string ITEM_PROPERTY_NAME = "Item";

		private readonly T_OPTIONS _options;

		/// <summary>Construct a type emitter from its options.</summary>
		/// <remarks>Clones the options to avoid alteration after construction.</remarks>
		/// <param name="options"></param>
		public TypeEmitterBase(T_OPTIONS options)
		{
			if (options == null)
			{
				throw new ArgumentNullException("options");
			}

			_options = (T_OPTIONS)options.Clone();
			_options.Validate();
		}

		/// <summary>Exposes the options defining the emitter.</summary>
		public T_OPTIONS Options
		{
			get { return _options; }
		}

		/// <summary>Returns an instance of a factory of the component type the type emitter is configured to produce.</summary>
		/// <param name="componentTypeName"></param>
		/// <param name="factoryTypeName"></param>
		/// <param name="moduleBuilder"></param>
		/// <returns></returns>
		public abstract object EmitFactory(
			string componentTypeName,
			string factoryTypeName,
			ModuleBuilderExtender moduleBuilder);

		#region CreateFactoryInstance Methods
		/// <summary>Helper method emitting a factory type and creating an instance of it.</summary>
		/// <remarks>Induces the constructor to use as the only public one.  If more or less than one is one, it throws.</remarks>
		/// <param name="factoryTypeName"></param>
		/// <param name="typeToCreate"></param>
		/// <param name="interfaceTypeToReturn"></param>
		/// <param name="moduleBuilder"></param>
		/// <returns></returns>
		protected object CreateFactoryInstance(
			string factoryTypeName,
			Type typeToCreate,
			Type interfaceTypeToReturn,
			ModuleBuilderExtender moduleBuilder)
		{
			ConstructorInfo[] constructorInfoList = typeToCreate.GetConstructors();

			if (constructorInfoList.Length != 1)
			{
				throw new ApplicationException(string.Format(
					"Type {0} doesn't have one and only one constructor, it has {1}.",
					typeToCreate.Name,
					constructorInfoList.Length));
			}
			else
			{
				Type[] constructorParameterTypeList = Array.ConvertAll(
					constructorInfoList[0].GetParameters(),
					delegate(ParameterInfo info) { return info.ParameterType; });

				return CreateFactoryInstance(
					factoryTypeName,
					typeToCreate,
					interfaceTypeToReturn,
					moduleBuilder,
					constructorParameterTypeList);
			}
		}

		/// <summary>Helper method emitting a factory type and creating an instance of it.</summary>
		/// <param name="factoryTypeName"></param>
		/// <param name="typeToCreate"></param>
		/// <param name="interfaceTypeToReturn"></param>
		/// <param name="moduleBuilder"></param>
		/// <param name="constructorParameterTypeList"></param>
		/// <returns></returns>
		protected object CreateFactoryInstance(
			string factoryTypeName,
			Type typeToCreate,
			Type interfaceTypeToReturn,
			ModuleBuilderExtender moduleBuilder,
			params Type[] constructorParameterTypeList)
		{
			ConstructorInfo constructorInfo = typeToCreate.GetConstructor(constructorParameterTypeList);

			if (constructorInfo == null)
			{
				throw new ApplicationException(string.Concat(
					"typeToCreate doesn't have a constructor with parameters {",
					string.Join(", ", Array.ConvertAll(constructorParameterTypeList, delegate(Type t) { return t.Name; })),
					"}."));
			}

			Type factoryTypeDefinition = TypeHelper.GetFactoryGenericTypeDefinition(constructorParameterTypeList.Length);
			Type factoryInterfaceType =
				factoryTypeDefinition.MakeGenericType(MergeTypeList(interfaceTypeToReturn, constructorParameterTypeList));
			TypeBuilderExtender typeBuilder =
				moduleBuilder.CreateType(factoryTypeName, typeof(object), new Type[] { factoryInterfaceType });
			MethodInfo methodInfo = factoryInterfaceType.GetMethods()[0];
			ILGeneratorExtender ilGenerator = typeBuilder.ImplementInterfaceMethod(methodInfo, true);

			for (short i = 0; i != constructorParameterTypeList.Length; ++i)
			{
				ilGenerator.StackMethodParameter((short)(i + 1));
			}
			ilGenerator.StackNew(constructorInfo);
			ilGenerator.ReturnStack();

			Type factoryType = typeBuilder.TypeBuilder.CreateType();

			return Activator.CreateInstance(factoryType);
		}
		#endregion

		#region Interface Implementation
		/// <summary>
		/// Implements an interface and each of its parent interfaces (except a list of excluded interfaces)
		/// using delegates for emitting the code.
		/// </summary>
		/// <remarks>
		/// Basically this method does the plumbing and orchestration for implementing the type but leaves
		/// the method body implementation to delegates.
		/// </remarks>
		/// <param name="typeBuilder"></param>
		/// <param name="interfaceType"></param>
		/// <param name="emitGetMethod"></param>
		/// <param name="emitSetMethod"></param>
		/// <param name="emitAddEventHandlerMethod"/>
		/// <param name="emitRemoveEventHandlerMethod"/>
		/// <param name="emitMethod"></param>
		/// <param name="excludedInterfaceTypeList"></param>
		protected void ImplementInterface(
			TypeBuilderExtender typeBuilder,
			Type interfaceType,
			EmitPropertyHandler emitGetMethod,
			EmitPropertyHandler emitSetMethod,
			EmitEventHandler emitAddEventHandlerMethod,
			EmitEventHandler emitRemoveEventHandlerMethod,
			EmitMethodHandler emitMethod,
			params Type[] excludedInterfaceTypeList)
		{
			if (typeBuilder == null)
			{
				throw new ArgumentNullException("typeBuilder");
			}
			if (interfaceType == null)
			{
				throw new ArgumentNullException("interfaceType");
			}
			if (!interfaceType.IsInterface)
			{
				throw new ArgumentException(string.Format("'{0}' isn't an interface", interfaceType.Name), "interfaceType");
			}
			foreach (Type excludedInterfaceType in excludedInterfaceTypeList)
			{
				if (excludedInterfaceType == null)
				{
					throw new ArgumentNullException("excludedInterfaceTypeList");
				}
				if (!excludedInterfaceType.IsInterface)
				{
					throw new ArgumentException(
						string.Format("'{0}' isn't an interface", excludedInterfaceType.Name),
						"excludedInterfaceTypeList");
				}
			}
			Type[] allInterfaces = TypeHelper.GetAllInterfaces(interfaceType, true);

			foreach (Type subInterfaceType in allInterfaces)
			{   //  Make sure the interface isn't on the forbidden list
				if (!((IList<Type>)excludedInterfaceTypeList).Contains(subInterfaceType))
				{
					ImplementOneInterface(
						typeBuilder,
						subInterfaceType,
						emitGetMethod,
						emitSetMethod,
						emitAddEventHandlerMethod,
						emitRemoveEventHandlerMethod,
						emitMethod);
				}
			}
		}

		private void ImplementOneInterface(
			TypeBuilderExtender typeBuilder,
			Type interfaceType,
			EmitPropertyHandler emitGetMethod,
			EmitPropertyHandler emitSetMethod,
			EmitEventHandler emitAddEventHandlerMethod,
			EmitEventHandler emitRemoveEventHandlerMethod,
			EmitMethodHandler emitMethod)
		{
			SupportBaseClassTypeEmitterOptions options = Options as SupportBaseClassTypeEmitterOptions;
			Type baseTypeToUseMembers = options == null ? null : (options.IsUsingBaseClassMembers ? options.BaseType : null);
			List<MethodInfo> methodList = new List<MethodInfo>();

			//	Events
			methodList.AddRange(ImplementOneInterfaceEvents(
				typeBuilder,
				interfaceType,
				baseTypeToUseMembers,
				emitAddEventHandlerMethod,
				emitRemoveEventHandlerMethod));
			//	Properties
			methodList.AddRange(ImplementOneInterfaceProperties(
				typeBuilder,
				interfaceType,
				baseTypeToUseMembers,
				emitGetMethod,
				emitSetMethod));
			//	Methods
			ImplementOneInterfaceMethods(typeBuilder, interfaceType, baseTypeToUseMembers, emitMethod, methodList);
		}

		private void ImplementOneInterfaceMethods(
			TypeBuilderExtender typeBuilder,
			Type interfaceType,
			Type baseTypeToUseMembers,
			EmitMethodHandler emitMethod,
			IList<MethodInfo> methodList)
		{
			foreach (MethodInfo method in interfaceType.GetMethods())
			{
				MethodInfo baseMethod = GetBaseMethod(baseTypeToUseMembers, method);

				//  Ensures the method isn't a property
				if (!methodList.Contains(method))
				{
					ILGeneratorExtender ilGenerator =
						typeBuilder.ImplementInterfaceMethod(method, Options.IsPrivateImplementation);

					if (baseMethod == null)
					{
						if (emitMethod == null)
						{
							if (Options.IsEmittingThrowNotSupported)
							{
								ilGenerator.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
								ilGenerator.Throw();
								ilGenerator.ReturnStack();
							}
							else
							{
								throw new ArgumentNullException("emitMethod");
							}
						}
						else
						{
							emitMethod(ilGenerator, method);
						}
					}
					else
					{
						ilGenerator.CallMethodWithMethodParameters(baseMethod);
					}
				}
			}
		}

		private static MethodInfo GetBaseMethod(Type type, MethodInfo method)
		{
			if (type != null)
			{
				foreach (MethodInfo baseMethod in type.GetMethods(BASE_FIND_BINDING_FLAGS))
				{   //  Find the right method instead doing a get on it, to differentiate method overloads
					if (!baseMethod.IsPrivate
						&& baseMethod.Name == method.Name
						&& baseMethod.ReturnType == method.ReturnType
						&& TypeHelper.AreEquivalent(baseMethod.GetParameters(), method.GetParameters()))
					{
						return baseMethod;
					}
				}
			}

			return null;
		}

		private MethodInfo[] ImplementOneInterfaceProperties(
			TypeBuilderExtender typeBuilder,
			Type interfaceType,
			Type baseTypeToUseMembers,
			EmitPropertyHandler emitGetMethod,
			EmitPropertyHandler emitSetMethod)
		{
			List<MethodInfo> methodList = new List<MethodInfo>();

			foreach (PropertyInfo property in interfaceType.GetProperties())
			{
				PropertyInfo baseProperty = GetBaseProperty(baseTypeToUseMembers, property);
				ILGeneratorExtender ilGeneratorGet;
				ILGeneratorExtender ilGeneratorSet;

				typeBuilder.ImplementProperty(
					property, Options.IsPrivateImplementation, out ilGeneratorGet, out ilGeneratorSet);
				if (baseProperty == null)
				{
					if (ilGeneratorGet != null)
					{
						if (emitGetMethod == null)
						{
							if (Options.IsEmittingThrowNotSupported)
							{
								ilGeneratorGet.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
								ilGeneratorGet.Throw();
								ilGeneratorGet.ReturnStack();
							}
							else
							{
								throw new ArgumentNullException("emitGetMethod");
							}
						}
						else
						{
							emitGetMethod(ilGeneratorGet, property.GetGetMethod(), property);
						}
					}
					if (ilGeneratorSet != null)
					{
						if (emitSetMethod == null)
						{
							if (Options.IsEmittingThrowNotSupported)
							{
								ilGeneratorSet.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
								ilGeneratorSet.Throw();
								ilGeneratorSet.ReturnStack();
							}
							else
							{
								throw new ArgumentNullException("emitSetMethod");
							}
						}
						else
						{
							emitSetMethod(ilGeneratorSet, property.GetSetMethod(), property);
						}
					}
				}
				else
				{
					if (ilGeneratorGet != null)
					{
						ilGeneratorGet.CallMethodWithMethodParameters(baseProperty.GetGetMethod(true));
					}
					if (ilGeneratorSet != null)
					{
						ilGeneratorSet.CallMethodWithMethodParameters(baseProperty.GetSetMethod(true));
					}
				}
				if (property.CanRead)
				{
					methodList.Add(property.GetGetMethod());
				}
				if (property.CanWrite)
				{
					methodList.Add(property.GetSetMethod());
				}
			}

			return methodList.ToArray();
		}

		private static PropertyInfo GetBaseProperty(Type type, PropertyInfo property)
		{
			if (type != null)
			{
				PropertyInfo baseProperty = type.GetProperty(property.Name, BASE_FIND_BINDING_FLAGS);

				if (baseProperty != null
					&& baseProperty.PropertyType == property.PropertyType
					&& baseProperty.CanRead == property.CanRead
					&& baseProperty.CanWrite == property.CanWrite
					&& (!baseProperty.CanRead || !baseProperty.GetGetMethod(true).IsPrivate)
					&& (!baseProperty.CanWrite || !baseProperty.GetSetMethod(true).IsPrivate))
				{
					return baseProperty;
				}
			}

			return null;
		}

		private MethodInfo[] ImplementOneInterfaceEvents(
			TypeBuilderExtender typeBuilder,
			Type interfaceType,
			Type baseTypeToUseMembers,
			EmitEventHandler emitAddEventHandlerMethod,
			EmitEventHandler emitRemoveEventHandlerMethod)
		{
			List<MethodInfo> methodList = new List<MethodInfo>();

			foreach (EventInfo eventInfo in interfaceType.GetEvents())
			{
				EventInfo baseEventInfo = GetBaseEvent(baseTypeToUseMembers, eventInfo);
				ILGeneratorExtender ilGeneratorAdd;
				ILGeneratorExtender ilGeneratorRemove;

				typeBuilder.ImplementEvent(
					eventInfo, Options.IsPrivateImplementation, out ilGeneratorAdd, out ilGeneratorRemove);
				if (baseEventInfo == null)
				{
					if (emitAddEventHandlerMethod == null)
					{
						if (Options.IsEmittingThrowNotSupported)
						{
							ilGeneratorAdd.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
							ilGeneratorAdd.Throw();
							ilGeneratorAdd.ReturnStack();
						}
						else
						{
							throw new ArgumentNullException("emitAddEventHandlerMethod");
						}
					}
					if (emitRemoveEventHandlerMethod == null)
					{
						if (Options.IsEmittingThrowNotSupported)
						{
							ilGeneratorRemove.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
							ilGeneratorRemove.Throw();
							ilGeneratorRemove.ReturnStack();
						}
						else
						{
							throw new ArgumentNullException("emitRemoveEventHandlerMethod");
						}
					}

					emitAddEventHandlerMethod(ilGeneratorAdd, eventInfo.GetAddMethod(), eventInfo);
					emitRemoveEventHandlerMethod(ilGeneratorRemove, eventInfo.GetRemoveMethod(), eventInfo);
				}
				else
				{
					ilGeneratorAdd.CallMethodWithMethodParameters(baseEventInfo.GetAddMethod(true));
					ilGeneratorRemove.CallMethodWithMethodParameters(baseEventInfo.GetRemoveMethod(true));
				}
				methodList.Add(eventInfo.GetAddMethod());
				methodList.Add(eventInfo.GetRemoveMethod());
			}

			return methodList.ToArray();
		}

		private static EventInfo GetBaseEvent(Type type, EventInfo eventInfo)
		{
			if (type != null)
			{
				EventInfo baseEvent = type.GetEvent(eventInfo.Name, BASE_FIND_BINDING_FLAGS);

				if (
					baseEvent != null
					&& baseEvent.EventHandlerType == eventInfo.EventHandlerType
					&& !baseEvent.GetAddMethod(true).IsPrivate
					&& !baseEvent.GetRemoveMethod(true).IsPrivate)
				{
					return baseEvent;
				}
			}

			return null;
		}
		#endregion

		#region IAdapter Implementation
		/// <summary>Defines the adaptee private field.</summary>
		/// <param name="adapteeType">Type of the adaptee, the object being adapted.</param>
		/// <returns>The generic interface.</returns>
		protected Type GetAdapterInterfaceType(Type adapteeType)
		{
			Type adapterInterfaceType = typeof(IAdapter<int>).GetGenericTypeDefinition().MakeGenericType(adapteeType);

			return adapterInterfaceType;
		}

		/// <summary>Defines the adaptee private field and implement the adapter interface.</summary>
		/// <param name="typeBuilder"></param>
		/// <param name="adapteeType">Type of the adaptee, the object being adapted.</param>
		protected void ImplementAdapterInterface(TypeBuilderExtender typeBuilder, Type adapteeType)
		{
			Type adapterInterfaceType = GetAdapterInterfaceType(adapteeType);

			typeBuilder.DefinePrivateField(ADAPTEE_FIELD_NAME, adapteeType, true, false);
			ImplementInterface(typeBuilder, adapterInterfaceType, EmitAdapterGetMethod, null, null, null, null);
		}

		/// <summary>Defines a constructor taking both an adaptee and base constructor parameter type list.</summary>
		/// <param name="typeBuilder"></param>
		/// <param name="adapteeType"></param>
		/// <param name="baseType"></param>
		/// <param name="baseConstructorParameterTypeList"></param>
		protected void DefineAdapterConstructor(
			TypeBuilderExtender typeBuilder,
			Type adapteeType,
			Type baseType,
			Type[] baseConstructorParameterTypeList)
		{
			MethodParameter adapteeParameter = new MethodParameter("adaptee", adapteeType);
			ILGeneratorExtender constructorGenerator = CreateInstanceConstructor(
				typeBuilder,
				baseType,
				baseConstructorParameterTypeList,
				adapteeParameter);

			constructorGenerator.ValidateNotNullParameter(1, adapteeParameter.Name);
			//  Store adaptee in instance field
			constructorGenerator.StackMethodParameter(0);
			constructorGenerator.StackMethodParameter(1);
			constructorGenerator.StoreStackInField(ADAPTEE_FIELD_NAME);
			constructorGenerator.ReturnStack();
		}

		private void EmitAdapterGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
		{
			ilGenerator.StackField(ADAPTEE_FIELD_NAME);
			ilGenerator.ReturnStack();
		}
		#endregion

		#region Constructor Implementation
		/// <summary>Defines a constructor taking a base constructor parameter type list.</summary>
		/// <param name="typeBuilder"></param>
		/// <param name="baseType"></param>
		/// <param name="baseConstructorParameterTypeList"></param>
		protected void DefineInstanceConstructor(
			TypeBuilderExtender typeBuilder,
			Type baseType,
			Type[] baseConstructorParameterTypeList)
		{
			ILGeneratorExtender constructorGenerator = CreateInstanceConstructor(
				typeBuilder,
				baseType,
				baseConstructorParameterTypeList);

			constructorGenerator.ReturnStack();
		}

		/// <summary>Creates a constructor and call the base class constructor.</summary>
		/// <param name="typeBuilder"></param>
		/// <param name="baseType"></param>
		/// <param name="baseConstructorParameterTypeList"></param>
		/// <param name="beforeBaseParameters"></param>
		/// <returns></returns>
		protected ILGeneratorExtender CreateInstanceConstructor(
			TypeBuilderExtender typeBuilder,
			Type baseType,
			Type[] baseConstructorParameterTypeList,
			params MethodParameter[] beforeBaseParameters)
		{
			List<MethodParameter> parameterList = new List<MethodParameter>();

			parameterList.AddRange(beforeBaseParameters);
			for (int i = 0; i != baseConstructorParameterTypeList.Length; ++i)
			{
				parameterList.Add(new MethodParameter(
					string.Concat("baseParam", (i + 1).ToString()),
					baseConstructorParameterTypeList[i]));
			}

			ILGeneratorExtender constructorGenerator = typeBuilder.DefinePublicInstanceConstructor(parameterList.ToArray());
			ConstructorInfo baseConstructor = baseType.GetConstructor(baseConstructorParameterTypeList);

			//  Call Base class constructor
			constructorGenerator.StackMethodParameter(0);
			for (int i = 0; i != baseConstructorParameterTypeList.Length; ++i)
			{
				constructorGenerator.StackMethodParameter((short)(i + 1 + beforeBaseParameters.Length));
			}
			constructorGenerator.CallConstructor(baseConstructor);

			return constructorGenerator;
		}
		#endregion

		private static Type[] MergeTypeList(Type firstType, IEnumerable<Type> tailTypeList)
		{
			var typeList = new List<Type>();

			typeList.Add(firstType);
			typeList.AddRange(tailTypeList);

			return typeList.ToArray();
		}
	}
}